package health_system.health_system.service.impl;

import health_system.health_system.dto.PageResponseDTO;
import health_system.health_system.dto.UserCreateDTO;
import health_system.health_system.dto.UserDTO;
import health_system.health_system.dto.UserUpdateDTO;
import health_system.health_system.entity.User;
import health_system.health_system.exception.BusinessException;
import health_system.health_system.exception.UserNotFoundException;
import health_system.health_system.repository.UserRepository;
import health_system.health_system.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "users", allEntries = true),
        @CacheEvict(value = "usersByUsername", allEntries = true),
        @CacheEvict(value = "usersByEmail", allEntries = true),
        @CacheEvict(value = "usersByPhoneNumber", allEntries = true)
    })
    public UserDTO createUser(UserCreateDTO createDTO) {
        log.debug("Creating user with username: {}", createDTO.getUsername());
        
        // Check if username already exists
        if (userRepository.existsByUsername(createDTO.getUsername())) {
            throw new BusinessException("Username already exists: " + createDTO.getUsername());
        }
        
        // Check if email already exists
        if (createDTO.getEmail() != null && userRepository.existsByEmail(createDTO.getEmail())) {
            throw new BusinessException("Email already exists: " + createDTO.getEmail());
        }
        
        // Check if phone number already exists
        if (createDTO.getPhoneNumber() != null && userRepository.existsByPhoneNumber(createDTO.getPhoneNumber())) {
            throw new BusinessException("Phone number already exists: " + createDTO.getPhoneNumber());
        }
        
        User user = new User();
        user.setUsername(createDTO.getUsername());
        user.setPassword(passwordEncoder.encode(createDTO.getPassword()));
        user.setFullName(createDTO.getFullName());
        user.setAvatarUrl(createDTO.getAvatarUrl());
        user.setEmail(createDTO.getEmail());
        user.setPhoneNumber(createDTO.getPhoneNumber());
        user.setGender(createDTO.getGender());
        user.setDateOfBirth(createDTO.getDateOfBirth());
        user.setBio(createDTO.getBio());
//        user.setRole(createDTO.getRole());
        user.setIsDeleted(false);
        
        try {
            User savedUser = userRepository.save(user);
            log.info("Successfully created user with ID: {}", savedUser.getId());
            return UserDTO.fromEntity(savedUser);
        } catch (DataIntegrityViolationException e) {
            log.error("Failed to create user due to data integrity violation", e);
            throw new BusinessException("Failed to create user. Please check your input data.", e);
        }
    }

    @Override
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "users", key = "#id"),
        @CacheEvict(value = "usersByUsername", allEntries = true),
        @CacheEvict(value = "usersByEmail", allEntries = true),
        @CacheEvict(value = "usersByPhoneNumber", allEntries = true)
    })
    public UserDTO updateUser(Long id, UserUpdateDTO updateDTO) {
        log.debug("Updating user with ID: {}", id);
        
        User user = getUserEntityById(id);
        
        // Check if new username already exists
        if (updateDTO.getUsername() != null && !updateDTO.getUsername().equals(user.getUsername())) {
            if (userRepository.existsByUsername(updateDTO.getUsername())) {
                throw new BusinessException("Username already exists: " + updateDTO.getUsername());
            }
        }
        
        // Check if new email already exists
        if (updateDTO.getEmail() != null && !updateDTO.getEmail().equals(user.getEmail())) {
            if (userRepository.existsByEmail(updateDTO.getEmail())) {
                throw new BusinessException("Email already exists: " + updateDTO.getEmail());
            }
        }
        
        // Check if new phone number already exists
        if (updateDTO.getPhoneNumber() != null && !updateDTO.getPhoneNumber().equals(user.getPhoneNumber())) {
            if (userRepository.existsByPhoneNumber(updateDTO.getPhoneNumber())) {
                throw new BusinessException("Phone number already exists: " + updateDTO.getPhoneNumber());
            }
        }
        
        // Update fields if provided
        if (updateDTO.getUsername() != null) {
            user.setUsername(updateDTO.getUsername());
        }
        
        if (updateDTO.getPassword() != null) {
            user.setPassword(passwordEncoder.encode(updateDTO.getPassword()));
        }
        
        if (updateDTO.getFullName() != null) {
            user.setFullName(updateDTO.getFullName());
        }
        
        if (updateDTO.getAvatarUrl() != null) {
            user.setAvatarUrl(updateDTO.getAvatarUrl());
        }
        
        if (updateDTO.getEmail() != null) {
            user.setEmail(updateDTO.getEmail());
        }
        
        if (updateDTO.getPhoneNumber() != null) {
            user.setPhoneNumber(updateDTO.getPhoneNumber());
        }
        
        if (updateDTO.getGender() != null) {
            user.setGender(updateDTO.getGender());
        }
        
        if (updateDTO.getDateOfBirth() != null) {
            user.setDateOfBirth(updateDTO.getDateOfBirth());
        }
        
        if (updateDTO.getBio() != null) {
            user.setBio(updateDTO.getBio());
        }
        
//        if (updateDTO.getRole() != null) {
//            user.setRole(updateDTO.getRole());
//        }
        
        try {
            User updatedUser = userRepository.save(user);
            log.info("Successfully updated user with ID: {}", updatedUser.getId());
            return UserDTO.fromEntity(updatedUser);
        } catch (DataIntegrityViolationException e) {
            log.error("Failed to update user due to data integrity violation", e);
            throw new BusinessException("Failed to update user. Please check your input data.", e);
        }
    }

    @Override
    @Transactional(readOnly = true)
    public UserDTO getUserById(Long id) {
        log.debug("Request to get User by id : {}", id);
        User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found"));
        return UserDTO.fromEntity(user);
    }

    @Override
    @Transactional(readOnly = true)
    public User getUserEntityById(Long id) {
        log.debug("Getting user entity by ID: {}", id);
        
        return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException(id));
    }

    @Override
    @Transactional(readOnly = true)
    public Optional<User> findUserEntityById(Long id) {
        log.debug("Finding user entity by ID: {}", id);
        
        return userRepository.findById(id);
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "usersByUsername", key = "#username")
    public UserDTO getUserByUsername(String username) {
        log.debug("Getting user by username: {}", username);
        
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UserNotFoundException("username", username));
        
        return UserDTO.fromEntity(user);
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "usersByEmail", key = "#email")
    public UserDTO getUserByEmail(String email) {
        log.debug("Getting user by email: {}", email);
        
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new UserNotFoundException("email", email));
        
        return UserDTO.fromEntity(user);
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "usersByPhoneNumber", key = "#phoneNumber")
    public UserDTO getUserByPhoneNumber(String phoneNumber) {
        log.debug("Getting user by phone number: {}", phoneNumber);
        
        User user = userRepository.findByPhoneNumber(phoneNumber)
                .orElseThrow(() -> new UserNotFoundException("phoneNumber", phoneNumber));
        
        return UserDTO.fromEntity(user);
    }

    @Override
    @Transactional
    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        log.debug("Soft deleting user with ID: {}", id);
        
        User user = getUserEntityById(id);
        user.setIsDeleted(true);
        
        try {
            userRepository.save(user);
            log.info("Successfully soft deleted user with ID: {}", id);
        } catch (Exception e) {
            log.error("Failed to soft delete user with ID: {}", id, e);
            throw new BusinessException("Failed to delete user with ID: " + id, e);
        }
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "allUsers")
    public List<UserDTO> getAllUsers() {
        log.debug("Getting all users");
        
        List<User> users = userRepository.findAll();
        
        if (users.isEmpty()) {
            return Collections.emptyList();
        }
        
        return users.stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "allUsersPaged", key = "'page:' + #pageable.pageNumber + '-size:' + #pageable.pageSize")
    public PageResponseDTO<UserDTO> getAllUsers(Pageable pageable) {
        log.debug("Getting paginated users with page: {}", pageable.getPageNumber());
        
        Page<User> usersPage = userRepository.findAll(pageable);
        
        Page<UserDTO> dtoPage = usersPage.map(UserDTO::fromEntity);
        
        return PageResponseDTO.fromPage(dtoPage);
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "usersByKeyword", key = "#keyword")
    public List<UserDTO> searchUsers(String keyword) {
        log.debug("Searching users with keyword: {}", keyword);
        
        List<User> users = userRepository.searchByKeyword(keyword);
        
        if (users.isEmpty()) {
            return Collections.emptyList();
        }
        
        return users.stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "usersByKeywordPaged", 
              key = "#keyword + '-page:' + #pageable.pageNumber + '-size:' + #pageable.pageSize")
    public PageResponseDTO<UserDTO> searchUsers(String keyword, Pageable pageable) {
        log.debug("Searching users with keyword: {} and page: {}", keyword, pageable.getPageNumber());
        
        List<User> users = userRepository.searchByKeyword(keyword);
        
        if (users.isEmpty()) {
            return PageResponseDTO.<UserDTO>builder()
                    .content(Collections.emptyList())
                    .pageNo(pageable.getPageNumber())
                    .pageSize(pageable.getPageSize())
                    .totalElements(0)
                    .totalPages(0)
                    .last(true)
                    .build();
        }
        
        // Manual pagination (since the repository method doesn't support pagination)
        int start = (int) pageable.getOffset();
        int end = Math.min((start + pageable.getPageSize()), users.size());
        
        if (start > users.size()) {
            return PageResponseDTO.<UserDTO>builder()
                    .content(Collections.emptyList())
                    .pageNo(pageable.getPageNumber())
                    .pageSize(pageable.getPageSize())
                    .totalElements(users.size())
                    .totalPages((int) Math.ceil((double) users.size() / pageable.getPageSize()))
                    .last(true)
                    .build();
        }
        
        List<User> pageItems = users.subList(start, end);
        List<UserDTO> dtoItems = pageItems.stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
        
        Page<UserDTO> dtoPage = new PageImpl<>(dtoItems, pageable, users.size());
        
        return PageResponseDTO.fromPage(dtoPage);
    }

    @Override
    @Transactional(readOnly = true)
    public boolean existsByUsername(String username) {
        log.debug("Checking if user exists by username: {}", username);
        
        return userRepository.existsByUsername(username);
    }

    @Override
    @Transactional(readOnly = true)
    public boolean existsByEmail(String email) {
        log.debug("Checking if user exists by email: {}", email);
        
        return userRepository.existsByEmail(email);
    }

    @Override
    @Transactional(readOnly = true)
    public boolean existsByPhoneNumber(String phoneNumber) {
        log.debug("Checking if user exists by phone number: {}", phoneNumber);
        
        return userRepository.existsByPhoneNumber(phoneNumber);
    }

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = "activeUsers")
    public List<UserDTO> getActiveUsers() {
        log.debug("Getting all active users");
        
        List<User> users = userRepository.findByIsDeletedFalse();
        
        if (users.isEmpty()) {
            return Collections.emptyList();
        }
        
        return users.stream()
                .map(UserDTO::fromEntity)
                .collect(Collectors.toList());
    }

    @Override
    @Transactional
    @CacheEvict(value = "users", key = "#id")
    public UserDTO changePassword(Long id, String currentPassword, String newPassword) {
        log.debug("Changing password for user with ID: {}", id);
        
        User user = getUserEntityById(id);
        
        // Verify current password
        if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
            throw new BusinessException("Current password is incorrect");
        }
        
        user.setPassword(passwordEncoder.encode(newPassword));
        
        try {
            User updatedUser = userRepository.save(user);
            log.info("Successfully changed password for user with ID: {}", id);
            return UserDTO.fromEntity(updatedUser);
        } catch (Exception e) {
            log.error("Failed to change password for user with ID: {}", id, e);
            throw new BusinessException("Failed to change password for user with ID: " + id, e);
        }
    }

    @Override
    @Transactional
    @CacheEvict(value = "users", key = "#id")
    public UserDTO updateAvatar(Long id, String avatarUrl) {
        log.debug("Updating avatar for user with ID: {}", id);
        
        User user = getUserEntityById(id);
        user.setAvatarUrl(avatarUrl);
        
        try {
            User updatedUser = userRepository.save(user);
            log.info("Successfully updated avatar for user with ID: {}", id);
            return UserDTO.fromEntity(updatedUser);
        } catch (Exception e) {
            log.error("Failed to update avatar for user with ID: {}", id, e);
            throw new BusinessException("Failed to update avatar for user with ID: " + id, e);
        }
    }

    @Override
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "users", key = "#id"),
        @CacheEvict(value = "usersByEmail", allEntries = true),
        @CacheEvict(value = "usersByPhoneNumber", allEntries = true),
        @CacheEvict(value = "allUsers", allEntries = true),
        @CacheEvict(value = "allUsersPaged", allEntries = true),
        @CacheEvict(value = "activeUsers", allEntries = true)
    })
    public UserDTO updateProfile(Long id, String fullName, String bio, LocalDate dateOfBirth) {
        log.debug("Request to update user profile : {}", id);
        
        User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found"));
        
        // Update profile fields
        if (fullName != null) {
            user.setFullName(fullName);
        }
        
        if (bio != null) {
            user.setBio(bio);
        }
        
        if (dateOfBirth != null) {
            user.setDateOfBirth(dateOfBirth);
        }
        
        user = userRepository.save(user);
        log.debug("Updated user profile: {}", user);
        
        return UserDTO.fromEntity(user);
    }

    @Override
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "users", key = "#id"),
        @CacheEvict(value = "usersByEmail", allEntries = true),
        @CacheEvict(value = "usersByPhoneNumber", allEntries = true),
        @CacheEvict(value = "allUsers", allEntries = true),
        @CacheEvict(value = "allUsersPaged", allEntries = true),
        @CacheEvict(value = "activeUsers", allEntries = true)
    })
    public UserDTO updateContactInfo(Long id, String email, String phoneNumber) {
        log.debug("Request to update user contact info : {}", id);
        
        User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User with id " + id + " not found"));
        
        // Validate if email is available
        if (email != null && !email.equals(user.getEmail())) {
            if (userRepository.existsByEmail(email)) {
                throw new BusinessException("Email is already in use");
            }
            user.setEmail(email);
        }
        
        // Validate if phone number is available
        if (phoneNumber != null && !phoneNumber.equals(user.getPhoneNumber())) {
            if (userRepository.existsByPhoneNumber(phoneNumber)) {
                throw new BusinessException("Phone number is already in use");
            }
            user.setPhoneNumber(phoneNumber);
        }
        
        user = userRepository.save(user);
        log.debug("Updated user contact info: {}", user);
        
        return UserDTO.fromEntity(user);
    }
} 